package upbeat;

import upbeat.types.Cue;
import upbeat.types.CueComparator;
import upbeat.types.Measure;

import upbeat.jmusiclib.JMusicWrapper;
import upbeat.jmusiclib.JMusicWrapperPrinter;

import java.util.PriorityQueue;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;

import java.io.*;
import java.util.Scanner;


public class OfflineManager extends Manager {

	private OfflineInput mInput;
	private PriorityQueue<Cue> mQueue;
	
	public OfflineManager() {
		super();
		
		mInput = new OfflineInput();
		Comparator<Cue> comparator = new CueComparator();
        mQueue = new PriorityQueue<Cue>(10, comparator);
	}
	
	@Override
	public void AddCueToScore(Cue cueToAdd) {
		mQueue.add( cueToAdd );
		
	}

	@Override
	public void MakeSomeMusic() throws Exception {
		if( mConductor == null ) {
			throw new Exception("OfflineManager.MakeSomeMusic(): Must instantiate Conductor before calling me!");
		}
		
		if( mMusicians.size() == 0 ) {
			throw new Exception("OfflineManager.MakeSomeMusic(): Must create some Musicians before calling me!");
		}
		
		if( mOptions == null ) {
			throw new Exception("OfflineManager.MakeSomeMusic(): Must instantiate TempoOptions before calling me!");
		}
		
		if( jMusicWrapper == null ) {
			throw new Exception("OfflineManager.MakeSomeMusic(): Must set a jMusicWrapper before calling me!");
		}
		
		try {
			MakeCueList();
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		}
		try {
			DistributeCues();
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		}
		MakeFinalSong();
		Play();
	}
	
	/**
	 * This goes through the input one piece at a time.  It then calls {@link Conductor.Conduct} for
	 * the input.  The {@link Conductor} constructs the Cue list by calling {@link AddQueueToScore}.
	 * Once the input is finished, our {@link Cue} list will be constructed and we can then begin
	 * sending them off to {@link Musician}s.
	 */
	private void MakeCueList() throws Exception{
		int currentMeasure = 1;
		
		// If the user does not specify an input source, then we want to call Conduct exactly once.
		if( !mInput.InputSourceSet() ) {
			mConductor.Conduct(currentMeasure, 0.0, true);
		}
		else {
			while( !mInput.EndOfFile() ){
				double nextInput = mInput.GetNextInput();
				boolean done = mInput.EndOfFile();
				mConductor.Conduct(currentMeasure, nextInput, done);
				currentMeasure++;
			}
		}
	}
	
	/**
	 * This distributes the {@link Cue}s to the {@link Musician}s.  It just loops over the
	 * {@link PriorityQueue} of Cues and sends them off to the different Musicians.
	 * @throws Exception 
	 */
	private void DistributeCues() throws Exception {
		while( mQueue.size() > 0 ) {
			Cue nextCue = mQueue.poll();
			
			Iterator< MusicianInterface > iterator = mMusicians.iterator();
		    while (iterator.hasNext()) {
		    	iterator.next().processCue( nextCue.clone() );
		    }
		}
	}
	
	/**
	 * Once the {@link Conductor} has processed all the input and the {@link Musician}s have
	 * made the {@link Measure} data structures that will be used internally to represent our
	 * output song, we need to actually output the song.  This method goes through all the
	 * Musicians and creates the final song, measure by measure.  It then passes each measure
	 * to the {@link JMusicWrapper} object to be turned into a MIDI file.
	 */
	private void MakeFinalSong() {
		
		// We decided as a convention that for offline songs, we will produce
		// a song that has a number of measures equal to the size of the input
		// (in doubles) plus 32.  The extra 32 measures at the end are so
		// that we can have things like a finale at the end.
		int totalSongLength = mInput.HowMuchInputRead() + 32;
		
		for( int i = 0; i < totalSongLength; i++ ) {
			LinkedList<Measure> nextMeasure = new LinkedList<Measure>();
			Iterator< MusicianInterface > iterator = mMusicians.iterator();
		    while (iterator.hasNext()) {
		    	
		    	MusicianInterface mus = iterator.next();
		    	Measure meas = mus.GetNextMeasure();
		    	meas.setChannel(mus.getChannel());
		    	nextMeasure.add( meas );
		    	
		    }
		    
		    jMusicWrapper.AppendNextMeasure(nextMeasure);
		}
	}
	
	/**
	 * This takes all the measures collected in the {@link JMusicWrapper} object and sends them
	 * out as MIDI data.
	 */
	private void Play() {
		jMusicWrapper.play();
	}

	@Override
	public boolean SetInputSource(String source) {
		return mInput.SetInputSource(source);
		
	}
	
	@Override
	public void AddMusician( MusicianInterface musician ) {
		if( musician != null )
			mMusicians.addLast(musician);
	}

}

/**
 * The OfflineInput class is used by the OfflineManager to access data from a file.
 * An OfflineInput object needs to be provided with a path to an input file.  It then
 * opens the file and reads in the input (in our case whitespace delimited doubles).
 * From there, every time GetNextInput is called, it returns the next chunk of input
 * in the file.
 * 
 * @author Matt
 *
 */
class OfflineInput {
	private boolean mInputSet;
	private Scanner mInputSource;
	private int mInputRead;
	
	OfflineInput() {
		mInputSet = false;
		mInputRead = 0;
	}
	
	/**
	 * How many pieces of input have been read?
	 * @return	the number of doubles that have been read from the input
	 */
	int HowMuchInputRead(){
		return mInputRead;
	}
	
	/**
	 * Sets the input source.  This should be a whitespace delimited text file full of doubles.
	 * @param source	a string telling the source of input.  In the case that the input is a file,
	 * 					then source should be a path to a file on the hard drive.
	 * @return			returns true if successfully able to access input, false otherwise.
	 */
	boolean SetInputSource( String source ){
		try {
			mInputSource = new Scanner(new BufferedReader(new FileReader(source)));
			mInputSet = true;
		} catch (FileNotFoundException e) {
			System.out.printf("Error: Could not find input file %s.\n", source);
			e.printStackTrace();
			mInputSet = false;
		}

		return mInputSet;
	}
	
	/**
	 * Says whether or not a valid source of input has been set.
	 * @return	whether or not the source of input has been set.
	 */
	boolean InputSourceSet() {
		return mInputSet;
	}
	
	/**
	 * This returns whether or not we've reached the end of the input source.
	 * @return	whether or not we've reached the end of the input source.
	 */
	boolean EndOfFile() {
		if( mInputSource == null )
			return true;
		else {
			
			// Scan ahead until we reach the next valid input value.
			while( !(mInputSource.hasNextDouble()) && mInputSource.hasNext() ) {
				mInputSource.next();
	        }
			
			// If there is a next valid input value, return true, otherwise we're at the end!
			return !(mInputSource.hasNextDouble());
		}
	}
	
	/**
	 * This gets the next input.  Always make sure you are NOT at the end of the file (using {@link EndOfFile})
	 * before you call this as there is no special EOF double.
	 * @return	the next piece of input.
	 */
	double GetNextInput(){
		double ret = 0.0;
		
		if( mInputSource == null )
			return 0.0;
		
		while( !(mInputSource.hasNextDouble()) && mInputSource.hasNext() ) {
			mInputSource.next();
        }
		
		if( mInputSource.hasNext() )
			ret = mInputSource.nextDouble();
		
		mInputRead++;
		
		return ret;
	}
}